package cn.b.sky.flowable.utils;

import cn.b.sky.flowable.graph.ActivitiHistoryGraphBuilder;
import cn.b.sky.flowable.graph.Edge;
import cn.b.sky.flowable.graph.Graph;
import org.apache.commons.io.FilenameUtils;
import org.flowable.bpmn.constants.BpmnXMLConstants;
import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.Process;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.impl.HistoricActivityInstanceQueryImpl;
import org.flowable.engine.impl.cmd.GetBpmnModelCmd;
import org.flowable.engine.impl.cmd.GetDeploymentProcessDefinitionCmd;
import org.flowable.engine.impl.context.Context;
import org.flowable.engine.impl.persistence.entity.HistoricActivityInstanceEntityManager;
import org.flowable.engine.impl.persistence.entity.HistoricProcessInstanceEntityManager;
import org.flowable.engine.impl.persistence.entity.ResourceEntityManager;
import org.flowable.engine.impl.util.ProcessDefinitionUtil;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.task.service.impl.persistence.entity.HistoricTaskInstanceEntityManager;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D.Double;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 流程图绘制工具
 */
public class CustomProcessDiagramGenerator {
  public static final int OFFSET_SUBPROCESS = 5;

  public static final int AVG_NUMBER = 2;
  
  public static final int NUMBER_THREE = 3;
  
  public static final int OFFSET_TASK = 20;
  
  public static final int CIRCLE_RADIUS = 15;

  private static List<String> taskType = new ArrayList<String>();

  private static List<String> eventType = new ArrayList<String>();

  private static List<String> gatewayType = new ArrayList<String>();

  private static List<String> subProcessType = new ArrayList<String>();

  private static Color RUNNING_COLOR = Color.RED;

  private static Color HISTORY_COLOR = Color.GREEN;

  private static Color SKIP_COLOR = Color.GRAY;

  private static Stroke THICK_BORDER_STROKE = new BasicStroke(3.0f);

  private int minX;

  private int minY;

  public CustomProcessDiagramGenerator() {
    init();
  }

  protected static void init() {
    taskType.add(BpmnXMLConstants.ELEMENT_TASK_MANUAL);
    taskType.add(BpmnXMLConstants.ELEMENT_TASK_RECEIVE);
    taskType.add(BpmnXMLConstants.ELEMENT_TASK_SCRIPT);
    taskType.add(BpmnXMLConstants.ELEMENT_TASK_SEND);
    taskType.add(BpmnXMLConstants.ELEMENT_TASK_SERVICE);
    taskType.add(BpmnXMLConstants.ELEMENT_TASK_USER);

    gatewayType.add(BpmnXMLConstants.ELEMENT_GATEWAY_EXCLUSIVE);
    gatewayType.add(BpmnXMLConstants.ELEMENT_GATEWAY_INCLUSIVE);
    gatewayType.add(BpmnXMLConstants.ELEMENT_GATEWAY_EVENT);
    gatewayType.add(BpmnXMLConstants.ELEMENT_GATEWAY_PARALLEL);

    eventType.add("intermediateTimer");
    eventType.add("intermediateMessageCatch");
    eventType.add("intermediateSignalCatch");
    eventType.add("intermediateSignalThrow");
    eventType.add("messageStartEvent");
    eventType.add("startTimerEvent");
    eventType.add(BpmnXMLConstants.ELEMENT_ERROR);
    eventType.add(BpmnXMLConstants.ELEMENT_EVENT_START);
    eventType.add("errorEndEvent");
    eventType.add(BpmnXMLConstants.ELEMENT_EVENT_END);

    subProcessType.add(BpmnXMLConstants.ELEMENT_SUBPROCESS);
    subProcessType.add(BpmnXMLConstants.ELEMENT_CALL_ACTIVITY);
  }

  public InputStream generateDiagram(String processInstanceId) throws IOException {
    HistoricProcessInstance historicProcessInstance = Context.getCommandContext()
            .getSession(HistoricProcessInstanceEntityManager.class).findById(processInstanceId);
    String processDefinitionId = historicProcessInstance.getProcessDefinitionId();
    BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionId);
    Point point = getMinXAndMinY(bpmnModel);
    this.minX = point.x;
    this.minY = point.y;
    this.minX = (this.minX <= OFFSET_SUBPROCESS) ? OFFSET_SUBPROCESS : this.minX;
    this.minY = (this.minY <= OFFSET_SUBPROCESS) ? OFFSET_SUBPROCESS : this.minY;
    this.minX -= OFFSET_SUBPROCESS;
    this.minY -= OFFSET_SUBPROCESS;

    ProcessDefinition definition = new GetDeploymentProcessDefinitionCmd(processDefinitionId).execute(Context
        .getCommandContext());
    String diagramResourceName = definition.getDiagramResourceName();
    String deploymentId = definition.getDeploymentId();
    byte[] bytes = Context.getCommandContext().getSession(ResourceEntityManager.class)
        .findResourceByDeploymentIdAndResourceName(deploymentId, diagramResourceName).getBytes();
    InputStream originDiagram = new ByteArrayInputStream(bytes);
    BufferedImage image = ImageIO.read(originDiagram);

    HistoricActivityInstanceQueryImpl historicActivityInstanceQueryImpl = new HistoricActivityInstanceQueryImpl();
    historicActivityInstanceQueryImpl.processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime()
        .asc().listPage(0, 100);
    List<HistoricActivityInstance> activityInstances = Context.getCommandContext()
        .getSession(HistoricActivityInstanceEntityManager.class)
        .findHistoricActivityInstancesByQueryCriteria(historicActivityInstanceQueryImpl);

    this.drawHistoryFlow(image, processInstanceId);
    for (HistoricActivityInstance historicActivityInstance : activityInstances) {
      String historicActivityId = historicActivityInstance.getActivityId();
      GraphicInfo activity = bpmnModel.getGraphicInfo(historicActivityId);

      if(activity == null){
        continue;
      }
    
      if (historicActivityInstance.getEndTime() == null) {
        // 节点正在运行中
        signRunningNode(image, activity.getX() - this.minX, activity.getY() - this.minY, activity.getWidth(),
            activity.getHeight(), historicActivityInstance.getActivityType());
      } else {
        String deleteReason = null;

        if (historicActivityInstance.getTaskId() != null) {
          deleteReason = Context.getCommandContext().getSession(HistoricTaskInstanceEntityManager.class)
              .findById(historicActivityInstance.getTaskId()).getDeleteReason();
        }

        // 节点已经结束
        if ("跳过".equals(deleteReason)) {
          signSkipNode(image, activity.getX() - this.minX, activity.getY() - this.minY, activity.getWidth(),
              activity.getHeight(), historicActivityInstance.getActivityType());
        } else {
          signHistoryNode(image, activity.getX() - this.minX, activity.getY() - this.minY, activity.getWidth(),
              activity.getHeight(), historicActivityInstance.getActivityType());
        }
      }
    }

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    String formatName = getDiagramExtension(diagramResourceName);
    ImageIO.write(image, formatName, out);

    return new ByteArrayInputStream(out.toByteArray());
  }

  private static String getDiagramExtension(String diagramResourceName) {
    return FilenameUtils.getExtension(diagramResourceName);
  }

  /**
   * 标记运行节点
   * 
   * @param image
   *            原始图片
   * @param x
   *            左上角节点坐在X位置
   * @param y
   *            左上角节点坐在Y位置
   * @param width
   *            宽
   * @param height
   *            高
   * @param activityType
   *            节点类型
   */
  private static void signRunningNode(BufferedImage image, double x, double y, double width, double height, String activityType) {
    Color nodeColor = RUNNING_COLOR;
    Graphics2D graphics = image.createGraphics();

    try {
      drawNodeBorder(x, y, width, height, graphics, nodeColor, activityType);
    } finally {
      graphics.dispose();
    }
  }

  /**
   * 标记历史节点
   * 
   * @param image
   *            原始图片
   * @param x
   *            左上角节点坐在X位置
   * @param y
   *            左上角节点坐在Y位置
   * @param width
   *            宽
   * @param height
   *            高
   * @param activityType
   *            节点类型
   */
  private static void signHistoryNode(BufferedImage image, double x, double y, double width, double height, String activityType) {
    Color nodeColor = HISTORY_COLOR;
    Graphics2D graphics = image.createGraphics();

    try {
      drawNodeBorder(x, y, width, height, graphics, nodeColor, activityType);
    } finally {
      graphics.dispose();
    }
  }

  private static void signSkipNode(BufferedImage image, double x, double y, double width, double height, String activityType) {
    Color nodeColor = SKIP_COLOR;
    Graphics2D graphics = image.createGraphics();

    try {
      drawNodeBorder(x, y, width, height, graphics, nodeColor, activityType);
    } finally {
      graphics.dispose();
    }
  }

  /**
   * 绘制节点边框
   * 
   * @param x
   *            左上角节点坐在X位置
   * @param y
   *            左上角节点坐在Y位置
   * @param width
   *            宽
   * @param height
   *            高
   * @param graphics
   *            绘图对象
   * @param color
   *            节点边框颜色
   * @param activityType
   *            节点类型
   */
  protected static void drawNodeBorder(double x, double y, double width, double height, Graphics2D graphics, Color color,
      String activityType) {
    graphics.setPaint(color);
    graphics.setStroke(THICK_BORDER_STROKE);

    if (taskType.contains(activityType)) {
      drawTask(x, y, width, height, graphics);
    } else if (gatewayType.contains(activityType)) {
      drawGateway((int)x, (int)y, (int)width, (int)height, graphics);
    } else if (eventType.contains(activityType)) {
      drawEvent(x, y, width, height, graphics);
    } else if (subProcessType.contains(activityType)) {
      drawSubProcess(x, y, width, height, graphics);
    }
  }

  /**
   * 绘制任务
   */
  protected static void drawTask(double x, double y, double width, double height, Graphics2D graphics) {
    RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, OFFSET_TASK, OFFSET_TASK);
    graphics.draw(rect);
  }

  /**
   * 绘制网关
   */
  protected static void drawGateway(int x, int y, int width, int height, Graphics2D graphics) {
    Polygon rhombus = new Polygon();
    rhombus.addPoint(x, y + (height / AVG_NUMBER));
    rhombus.addPoint(x + (width / AVG_NUMBER), y + height);
    rhombus.addPoint(x + width, y + (height / AVG_NUMBER));
    rhombus.addPoint(x + (width / AVG_NUMBER), y);
    graphics.draw(rhombus);
  }

  /**
   * 绘制任务
   */
  protected static void drawEvent(double x, double y, double width, double height, Graphics2D graphics) {
    Double circle = new Double(x, y, width, height);
    graphics.draw(circle);
  }

  /**
   * 绘制子流程
   */
  protected static void drawSubProcess(double x, double y, double width, double height, Graphics2D graphics) {
    RoundRectangle2D rect = new RoundRectangle2D.Double(x + 1, y + 1, width - AVG_NUMBER, height - AVG_NUMBER, OFFSET_SUBPROCESS,
        OFFSET_SUBPROCESS);
    graphics.draw(rect);
  }

  public static Point getMinXAndMinY(BpmnModel bpmnModel) {
    // We need to calculate maximum values to know how big the image will be in its entirety
    double theMinX = java.lang.Double.MAX_VALUE;
    double theMinY = java.lang.Double.MAX_VALUE;

    for (Pool pool : bpmnModel.getPools()) {
      GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(pool.getId());
      theMinX = graphicInfo.getX();
      theMinY = graphicInfo.getY();
    }

    List<FlowNode> flowNodes = gatherAllFlowNodes(bpmnModel);

    for (FlowNode flowNode : flowNodes) {
      GraphicInfo flowNodeGraphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());

      if (flowNodeGraphicInfo.getX() < theMinX) {
        theMinX = flowNodeGraphicInfo.getX();
      }

      if (flowNodeGraphicInfo.getY() < theMinY) {
        theMinY = flowNodeGraphicInfo.getY();
      }

      for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
        List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());

        for (GraphicInfo graphicInfo : graphicInfoList) {

          if (graphicInfo.getX() < theMinX) {
            theMinX = graphicInfo.getX();
          }

          if (graphicInfo.getY() < theMinY) {
            theMinY = graphicInfo.getY();
          }
        }
      }
    }

    List<Artifact> artifacts = gatherAllArtifacts(bpmnModel);

    for (Artifact artifact : artifacts) {
      GraphicInfo artifactGraphicInfo = bpmnModel.getGraphicInfo(artifact.getId());

      if (artifactGraphicInfo != null) {

        if (artifactGraphicInfo.getX() < theMinX) {
          theMinX = artifactGraphicInfo.getX();
        }

        if (artifactGraphicInfo.getY() < theMinY) {
          theMinY = artifactGraphicInfo.getY();
        }
      }

      List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(artifact.getId());

      if (graphicInfoList != null) {
        for (GraphicInfo graphicInfo : graphicInfoList) {
          if (graphicInfo.getX() < theMinX) {
            theMinX = graphicInfo.getX();
          }

          if (graphicInfo.getY() < theMinY) {
            theMinY = graphicInfo.getY();
          }
        }
      }
    }

    int nrOfLanes = 0;

    for (Process process : bpmnModel.getProcesses()) {
      for (Lane l : process.getLanes()) {
        nrOfLanes++;

        GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(l.getId());

        if (graphicInfo.getX() < theMinX) {
          theMinX = graphicInfo.getX();
        }

        if (graphicInfo.getY() < theMinY) {
          theMinY = graphicInfo.getY();
        }
      }
    }

    // Special case, see http://jira.codehaus.org/browse/ACT-1431
    if (flowNodes.isEmpty() && bpmnModel.getPools().isEmpty() && nrOfLanes == 0) {
      // Nothing to show
      theMinX = 0;
      theMinY = 0;
    }

    return new Point((int) theMinX, (int) theMinY);
  }

  protected static List<Artifact> gatherAllArtifacts(BpmnModel bpmnModel) {
    List<Artifact> artifacts = new ArrayList<Artifact>();

    for (Process process : bpmnModel.getProcesses()) {
      artifacts.addAll(process.getArtifacts());
    }

    return artifacts;
  }

  protected static List<FlowNode> gatherAllFlowNodes(BpmnModel bpmnModel) {
    List<FlowNode> flowNodes = new ArrayList<FlowNode>();

    for (Process process : bpmnModel.getProcesses()) {
      flowNodes.addAll(gatherAllFlowNodes(process));
    }

    return flowNodes;
  }

  protected static List<FlowNode> gatherAllFlowNodes(FlowElementsContainer flowElementsContainer) {
    List<FlowNode> flowNodes = new ArrayList<FlowNode>();

    for (FlowElement flowElement : flowElementsContainer.getFlowElements()) {
      if (flowElement instanceof FlowNode) {
        flowNodes.add((FlowNode) flowElement);
      }

      if (flowElement instanceof FlowElementsContainer) {
        flowNodes.addAll(gatherAllFlowNodes((FlowElementsContainer) flowElement));
      }
    }

    return flowNodes;
  }

  public void drawHistoryFlow(BufferedImage image, String processInstanceId) {
    HistoricProcessInstance historicProcessInstance = Context.getCommandContext()
        .getSession(HistoricProcessInstanceEntityManager.class).findById(processInstanceId);
    String processDefinitionId = historicProcessInstance.getProcessDefinitionId();
    Graph graph = new ActivitiHistoryGraphBuilder(processInstanceId).build();

    for (Edge edge : graph.getEdges()) {
      drawSequenceFlow(image, processDefinitionId, edge.getName());
    }
  }

  public void drawSequenceFlow(BufferedImage image, String processDefinitionId, String sequenceFlowId) {
    GetBpmnModelCmd getBpmnModelCmd = new GetBpmnModelCmd(processDefinitionId);
    BpmnModel bpmnModel = getBpmnModelCmd.execute(Context.getCommandContext());

    Graphics2D graphics = image.createGraphics();
    graphics.setPaint(HISTORY_COLOR);
    graphics.setStroke(new BasicStroke(2f));

    try {
      List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlowId);

      int[] xPoints = new int[graphicInfoList.size()];
      int[] yPoints = new int[graphicInfoList.size()];

      for (int i = 1; i < graphicInfoList.size(); i++) {
        GraphicInfo graphicInfo = graphicInfoList.get(i);
        GraphicInfo previousGraphicInfo = graphicInfoList.get(i - 1);

        if (i == 1) {
          xPoints[0] = (int) previousGraphicInfo.getX() - minX;
          yPoints[0] = (int) previousGraphicInfo.getY() - minY;
        }

        xPoints[i] = (int) graphicInfo.getX() - minX;
        yPoints[i] = (int) graphicInfo.getY() - minY;
      }

      Path2D path = new Path2D.Double();

      for (int i = 0; i < xPoints.length; i++) {
        Integer anchorX = xPoints[i];
        Integer anchorY = yPoints[i];

        double targetX = anchorX;
        double targetY = anchorY;

        double ax = 0;
        double ay = 0;
        double bx = 0;
        double by = 0;
        double zx = 0;
        double zy = 0;

        if ((i > 0) && (i < (xPoints.length - 1))) {
          Integer cx = anchorX;
          Integer cy = anchorY;

          // pivot point of prev line
          double lineLengthY = yPoints[i] - yPoints[i - 1];

          // pivot point of prev line
          double lineLengthX = xPoints[i] - xPoints[i - 1];
          double lineLength = Math.sqrt(Math.pow(lineLengthY, AVG_NUMBER) + Math.pow(lineLengthX, AVG_NUMBER));
          double dx = (lineLengthX * CIRCLE_RADIUS) / lineLength;
          double dy = (lineLengthY * CIRCLE_RADIUS) / lineLength;
          targetX = targetX - dx;
          targetY = targetY - dy;

          // isDefaultConditionAvailable = isDefault && i == 1 && lineLength > 10;
          if ((lineLength < (AVG_NUMBER * CIRCLE_RADIUS)) && (i > 1)) {
            targetX = xPoints[i] - (lineLengthX / AVG_NUMBER);
            targetY = yPoints[i] - (lineLengthY / AVG_NUMBER);
          }

          // pivot point of next line
          lineLengthY = yPoints[i + 1] - yPoints[i];
          lineLengthX = xPoints[i + 1] - xPoints[i];
          lineLength = Math.sqrt(Math.pow(lineLengthY, AVG_NUMBER) + Math.pow(lineLengthX, AVG_NUMBER));

          if (lineLength < CIRCLE_RADIUS) {
            lineLength = CIRCLE_RADIUS;
          }

          dx = (lineLengthX * CIRCLE_RADIUS) / lineLength;
          dy = (lineLengthY * CIRCLE_RADIUS) / lineLength;

          double nextSrcX = xPoints[i] + dx;
          double nextSrcY = yPoints[i] + dy;

          if ((lineLength < (AVG_NUMBER * CIRCLE_RADIUS)) && (i < (xPoints.length - AVG_NUMBER))) {
            nextSrcX = xPoints[i] + (lineLengthX / AVG_NUMBER);
            nextSrcY = yPoints[i] + (lineLengthY / AVG_NUMBER);
          }

          double dx0 = (cx - targetX) / NUMBER_THREE;
          double dy0 = (cy - targetY) / NUMBER_THREE;
          ax = cx - dx0;
          ay = cy - dy0;

          double dx1 = (cx - nextSrcX) / NUMBER_THREE;
          double dy1 = (cy - nextSrcY) / NUMBER_THREE;
          bx = cx - dx1;
          by = cy - dy1;

          zx = nextSrcX;
          zy = nextSrcY;
        }

        if (i == 0) {
          path.moveTo(targetX, targetY);
        } else {
          path.lineTo(targetX, targetY);
        }

        if ((i > 0) && (i < (xPoints.length - 1))) {
          // add curve
          path.curveTo(ax, ay, bx, by, zx, zy);
        }
      }

      graphics.draw(path);

      // draw arrow
      Line2D.Double line = new Line2D.Double(xPoints[xPoints.length - AVG_NUMBER], yPoints[xPoints.length - AVG_NUMBER],
          xPoints[xPoints.length - 1], yPoints[xPoints.length - 1]);

      int arrowWidth = OFFSET_SUBPROCESS;
      int doubleArrowWidth = AVG_NUMBER * arrowWidth;
      Polygon arrowHead = new Polygon();
      arrowHead.addPoint(0, 0);
      arrowHead.addPoint(-arrowWidth, -doubleArrowWidth);
      arrowHead.addPoint(arrowWidth, -doubleArrowWidth);

      AffineTransform transformation = new AffineTransform();
      transformation.setToIdentity();

      double angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1);
      transformation.translate(line.x2, line.y2);
      transformation.rotate(angle - (Math.PI / 2d));

      AffineTransform originalTransformation = graphics.getTransform();
      graphics.setTransform(transformation);
      graphics.fill(arrowHead);
      graphics.setTransform(originalTransformation);
    } finally {
      graphics.dispose();
    }
  }
}
